{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在本小节的编程练习中，我们将使用Tensorflow构建Softmax分类器以及卷积神经网络。本节我们将逐步完成：\n",
    "* 如何创建Softmax分类器识别MNIST数字图形数据集；\n",
    "* 如何使用Tensorflow训练模型；\n",
    "* 如何使用Tensorflow测试模型精度；\n",
    "* 如何使用Tensorflow创建卷积神经网络并训练模型。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 载入 MNIST数据\n",
    "如下代码所示，我们可以使用Tensorflow自动的下载和读取MNIST数据集："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from tensorflow.examples.tutorials.mnist import input_data\n",
    "mnist = input_data.read_data_sets('MNIST_data', one_hot=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "minist是一个使用NumPy数组储存训练，验证，测试数据的轻量级类。其还提供了批量采样数据等方法，将会在接下来的内容中使用。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 启动 TensorFlow InteractiveSession\n",
    "Tensorflow使用高效的C++代码在后端进行计算任务，而session便是我们连接到后端的桥梁。在Tensorflow中，第一步通常会创建计算图，然后再使用session启动计算图。但和上节内容不同，接下来我们使用InteractiveSession类创建session，该类可以帮你在Tensorflow中更灵活的构建代码。如果你不使用InteractiveSession，那你需要在运行计算图前构建好完整的计算图。InteractiveSession允许你交错的构建和运行计算图，这在使用诸如IPython这样的交互式文本中显得非常的方便。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import tensorflow as tf\n",
    "sess = tf.InteractiveSession()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 计算图\n",
    "为了能够在Python中高效地进行数值计算，我们通常会使用Numpy库在Python的外部使用别的高效率语言，执行类似矩阵乘法这样的高负荷操作。但不幸的是，在每一次Python的前后端切换操作时都需要大量的额外开销。特别是使用GPU或者分布式策略时，这种数据转换开销将异常的耗时。Tensorflow同样采用在Python外部执行的方式提高效率，但其也通过其他一些措施来避免这种额外的开销。和在Python外运行单独的高负荷操作不同，Tensorflow允许我们描述交互操作图，然后将所有操作完全运行于Python之外。这种方式和Theano，Torch等主流深度学习库相似。因此在Tensorflow中，Python代码只是负责构建外部的计算图，然后控制计算图运行。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 构建 Softmax模型\n",
    "在本小节中，我们将通过一层线性层构建softmax模型。然后在下一节中，我们将以此基础构建卷积神经网络。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 占位符\n",
    "如下所示，我们从创建用于输入图像与输出类标的计算图节点开始："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "x = tf.placeholder(tf.float32, shape=[None, 784])\n",
    "y_ = tf.placeholder(tf.float32, shape=[None, 10])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "x 和 `y_` 没有特定的值，因此我们使用placeholder(占位符)进行声明，当Tensorflow运行计算图时，我们再动态的输入数据。输入图像x是一个2维的浮点数tensor，我们将其形状声明为[ None, 784 ]。其中784是一张28X28像素的MNIST图像的数据维度，None指的是tensor的第一维，也就是对应的图像批量尺寸可以是任意值。输出分类`y_`同样也是一个2维tensor，其每一行是10维的输出向量，表示MNIST图像对应的0-9数字类标。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 变量\n",
    "接下来，我们使用Variable(变量)定义模型的权重W与偏置项b。一个变量是驻留在Tensorflow计算图中的值，其可以在计算时被修改。在机器学习的语境中，我们也把变量称为参数。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "W = tf.Variable(tf.zeros([784,10]))\n",
    "b = tf.Variable(tf.zeros([10]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们通过调用tf.Variable函数将初始值传递到每一个参数中。在该例子中，我们将W和b都初始化为0。其中W是784x10的矩阵，而b是一个10维向量。变量在被使用之前，他们必须使用session进行初始化。这一步相当于将上述初始化的值(tf.zeros)传递到变量中。如下所示，我们可以使用tf.global_variables_initializer函数一次性地将变量全部初始化："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "sess.run(tf.global_variables_initializer())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 预测分类与损失函数\n",
    "接下来我们开始Softmax的编码。如下所示，我们只是用输入图像x与权重矩阵W进行乘法运算，然后加上偏置项b即可。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "y = tf.matmul(x,W) + b"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如下所示，我们使用交叉熵作为损失函数，该函数已经在Tensorflow中实现了，我们只需要传入计算图y与类标节点 `y_` 即可。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "cross_entropy = tf.reduce_mean(\n",
    "    tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "需要注意的是tf.nn.softmax_cross_entropy_with_logits内部使用的是非归一化Softmax模型进行预测并累加所有分类的得分。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 训练模型\n",
    "现在我们已经定义好了训练模型以及损失函数，接下来我们就是用Tensorflow进行训练。由于Tensorflow已经知道了完整的计算图，其可以依据损失值自动的求导，然后计算梯度修改权重变量。Tensorflow内置了许多的优化算法，如下所示，我们将学习率设置为0.5，使用最速梯度下降法降低交叉熵代价损失："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在上述的一行代码中，Tensorflow实际做的工作其实是在计算图中加入新的操作节点。但这些操作包括了：计算梯度，计算参数更新步数，更新参数等操作。其返回值trian_step也是一个操作，当运行计算图时，其将使用梯度更新参数。因此如下所示，训练模型可以重复的运行train_step来不断更新参数。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "for _ in range(1000):\n",
    "  batch = mnist.train.next_batch(100)\n",
    "  train_step.run(feed_dict={x: batch[0], y_: batch[1]})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "每次迭代时，我们载入100条训练数据进行训练。然后当我们运行train_step操作时，使用`feed_dict`去将占位符x与 `y_` 替换为载入的训练数据。需要注意的是，你可以在计算图中使用`feed_dict`替换任何tensor，不仅仅局限于占位符。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 评估模型\n",
    "在评估模型时，我们首先要预测类标。tf.argmax是一个非常有用的函数，其返回给定tensor某一坐标轴上最高得分的索引值。例如，tf.argmax( y, 1 )返回的是模型每一输入数据最大可能的预测类标，而`tf.argmax( y_, 1 )`返回的是真实的类标。最后我们使用tf.equal函数检查预测类标与真实类标的一致性。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "该返回值correct_prediction为一个布尔值链表。想要计算模型的精度，我们还需要计算该链表的均值。例如，[ True, False, True, True ]可以用[ 1, 0, 1, 1 ]表示，其精度为0.75。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "最后，我们使用测试数据评估我们模型的精确度。该测试结果大约在92%。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "print(accuracy.eval(feed_dict={x: mnist.test.images, y_: mnist.test.labels}))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 卷积神经网络\n",
    "虽然我们的Softmax分类器可以在MNIST数据集上实现92%的精确度，但这性能其实是比较差的。在本小节，我们将使用Tensorflow构建小型的卷积神经网络，其模型精度大约在99.2%。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 权重初始化\n",
    "首先，我们需要创建大量的权重和偏置项参数。如下所示，我们使用标准差为0.1的正太分布初始化权重，由于我们使用ReLU作为激活函数，为了避免神经元死亡现象，我们使用常数0.1初始化偏置项。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def weight_variable(shape):\n",
    "  initial = tf.truncated_normal(shape, stddev=0.1)\n",
    "  return tf.Variable(initial)\n",
    "\n",
    "def bias_variable(shape):\n",
    "  initial = tf.constant(0.1, shape=shape)\n",
    "  return tf.Variable(initial)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 卷积和池化\n",
    "Tensorflow的卷积和池化操作给予了我们很大的灵活性，我们可以自定义卷积核尺寸，跨步尺寸，零填充类型等功能。如下所示，在卷积操作中我们使用跨步为1，same零填充，进行卷积特征提取；在池化操作时，我们使用2x2的最大池化操作。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def conv2d(x, W):\n",
    "  return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')\n",
    "\n",
    "def max_pool_2x2(x):\n",
    "  return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],\n",
    "                        strides=[1, 2, 2, 1], padding='SAME')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 构造第一层卷积层\n",
    "接下来，我们实现第一层卷积层。我们使用32个5x5的卷积核作为第一层卷积特征提取层，因此第一层权重是形状为[5, 5, 1, 32]的张量。该张量的前两维表示卷积核的大小，第三维表示输入通道，第四维表示输出通道。而卷积层的偏置项为32维的向量，对应于每一输出通道。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "W_conv1 = weight_variable([5, 5, 1, 32])\n",
    "b_conv1 = bias_variable([32])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "为了使输入数据与卷积权重相对应，我们首先要将输入数据x重塑为一个4维张量。该张量的第二与第三维对应图像的宽和高，最后一维对应图像的色彩通道，第一维为图像的数量。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "x_image = tf.reshape(x, [-1,28,28,1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来我们就开始使用权重张量W_conv1与输入数据x_image进行卷积，然后加上偏置项使用ReLU函数进行激活，最后再进行最大池化。如下代码所示，max_pool_2x2函数将图片尺寸减少到14x14。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)\n",
    "h_pool1 = max_pool_2x2(h_conv1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 第二层卷积层\n",
    "如下所示，在第二层的卷积层中，我们使用64个5x5的卷积核进行特征提取，然后再使用2x2最大池化："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "W_conv2 = weight_variable([5, 5, 32, 64])\n",
    "b_conv2 = bias_variable([64])\n",
    "\n",
    "h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)\n",
    "h_pool2 = max_pool_2x2(h_conv2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 全连接层\n",
    "现在，我们已经将图片的尺寸降低到了7x7，接下来我们添加一层1024单元的全连接层进行特征提取。如下所示，全连接层的权重为3136x1024，其中3136为第二层卷积层的输出维度，1024为全连接层的输出维度。需要注意的是，当卷积层与全连接层进行对接的时候，需要将第二层卷积层输出的三维(宽，高，色道)的特征重塑为一维。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "W_fc1 = weight_variable([7 * 7 * 64, 1024])\n",
    "b_fc1 = bias_variable([1024])\n",
    "\n",
    "h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])\n",
    "h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Dropout\n",
    "为了降低过拟合现象，我们在输出层之前再加入一层dropout层。如下所示，我们使用占位符表示dropout的神经元激活概率，这允许我们可以在训练阶段开启dropout，而在测试阶段关闭dropout功能。Tensorflow的tf.nn.dropout函数已经实现了dropout操作，我们直接将其当作节点添加到计算图中即可。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "keep_prob = tf.placeholder(tf.float32)\n",
    "h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 输出层\n",
    "如下所示，卷积网络的输出层和Softmax一样，你也可以认为我们是在全连接层之后添加了一层Softmax层。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "W_fc2 = weight_variable([1024, 10])\n",
    "b_fc2 = bias_variable([10])\n",
    "\n",
    "y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练和评估卷积网络\n",
    "卷积网络的训练和评估过程与上述的Softmax相同，唯一的小区别只有以下的几个小点：\n",
    "* 我们将最速梯度下降优化器替换成了更复杂一些的Adam优化器；\n",
    "* 我们在feed_dict中添加了额外的keep_prob参数控制Dropout激活概率；\n",
    "* 在训练过程中，每500次迭代我们记录一次训练情况。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "cross_entropy = tf.reduce_mean(\n",
    "    tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y_conv))\n",
    "train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)\n",
    "correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))\n",
    "accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))\n",
    "sess.run(tf.global_variables_initializer())\n",
    "for i in range(10000):\n",
    "  batch = mnist.train.next_batch(50)\n",
    "  if i%500 == 0:\n",
    "    train_accuracy = accuracy.eval(feed_dict={\n",
    "        x:batch[0], y_: batch[1], keep_prob: 1.0})\n",
    "    print(\"步数 %d, 训练精确度： %g\"%(i, train_accuracy))\n",
    "  train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})\n",
    "\n",
    "print(\"测试精确度： %g\"%accuracy.eval(feed_dict={\n",
    "    x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "kernelspec": {
   "display_name": "Python [conda root]",
   "language": "python",
   "name": "conda-root-py"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
